/* *
 *
 *  (c) 2010-2019 Paweł Dalek
 *
 *  Volume By Price (VBP) indicator for Highstock
 *
 *  License: www.highcharts.com/license
 *
 * */

'use strict';

import H from '../parts/Globals.js';

import U from '../parts/Utilities.js';
var isArray = U.isArray;

// Utils
function arrayExtremesOHLC(data) {
    var dataLength = data.length,
        min = data[0][3],
        max = min,
        i = 1,
        currentPoint;

    for (; i < dataLength; i++) {
        currentPoint = data[i][3];
        if (currentPoint < min) {
            min = currentPoint;
        }

        if (currentPoint > max) {
            max = currentPoint;
        }
    }

    return {
        min: min,
        max: max
    };
}

var abs = Math.abs,
    noop = H.noop,
    addEvent = H.addEvent,
    correctFloat = H.correctFloat,
    seriesType = H.seriesType,
    columnPrototype = H.seriesTypes.column.prototype;

/**
 * The Volume By Price (VBP) series type.
 *
 * @private
 * @class
 * @name Highcharts.seriesTypes.vbp
 *
 * @augments Highcharts.Series
 */
seriesType(
    'vbp',
    'sma',
    /**
     * Volume By Price indicator.
     *
     * This series requires `linkedTo` option to be set.
     *
     * @sample stock/indicators/volume-by-price
     *         Volume By Price indicator
     *
     * @extends      plotOptions.sma
     * @since        6.0.0
     * @product      highstock
     * @optionparent plotOptions.vbp
     */
    {
        /**
         * @excluding index, period
         */
        params: {
            /**
             * The number of price zones.
             */
            ranges: 12,
            /**
             * The id of volume series which is mandatory. For example using
             * OHLC data, volumeSeriesID='volume' means the indicator will be
             * calculated using OHLC and volume values.
             */
            volumeSeriesID: 'volume'
        },
        /**
         * The styles for lines which determine price zones.
         */
        zoneLines: {
            /**
             * Enable/disable zone lines.
             */
            enabled: true,
            /**
             * Specify the style of zone lines.
             *
             * @type    {Highcharts.CSSObject}
             * @default {"color": "#0A9AC9", "dashStyle": "LongDash", "lineWidth": 1}
             */
            styles: {
                /** @ignore-options */
                color: '#0A9AC9',
                /** @ignore-options */
                dashStyle: 'LongDash',
                /** @ignore-options */
                lineWidth: 1
            }
        },
        /**
         * The styles for bars when volume is divided into positive/negative.
         */
        volumeDivision: {
            /**
             * Option to control if volume is divided.
             */
            enabled: true,
            styles: {
                /**
                 * Color of positive volume bars.
                 *
                 * @type {Highcharts.ColorString}
                 */
                positiveColor: 'rgba(144, 237, 125, 0.8)',
                /**
                 * Color of negative volume bars.
                 *
                 * @type {Highcharts.ColorString}
                 */
                negativeColor: 'rgba(244, 91, 91, 0.8)'
            }
        },
        // To enable series animation; must be animationLimit > pointCount
        animationLimit: 1000,
        enableMouseTracking: false,
        pointPadding: 0,
        zIndex: -1,
        crisp: true,
        dataGrouping: {
            enabled: false
        },
        dataLabels: {
            /** @ignore-option */
            allowOverlap: true,
            /** @ignore-option */
            enabled: true,
            /** @ignore-option */
            format: 'P: {point.volumePos:.2f} | N: {point.volumeNeg:.2f}',
            /** @ignore-option */
            padding: 0,
            /** @ignore-option */
            style: {
                fontSize: '7px'
            },
            /** @ignore-option */
            verticalAlign: 'top'
        }
    },
    /**
     * @lends Highcharts.Series#
     */
    {
        nameBase: 'Volume by Price',
        bindTo: {
            series: false,
            eventName: 'afterSetExtremes'
        },
        calculateOn: 'render',
        markerAttribs: noop,
        drawGraph: noop,
        getColumnMetrics: columnPrototype.getColumnMetrics,
        crispCol: columnPrototype.crispCol,
        init: function (chart) {
            var indicator = this,
                params,
                baseSeries,
                volumeSeries;

            H.seriesTypes.sma.prototype.init.apply(indicator, arguments);

            params = indicator.options.params;
            baseSeries = indicator.linkedParent;
            volumeSeries = chart.get(params.volumeSeriesID);

            indicator.addCustomEvents(baseSeries, volumeSeries);

            return indicator;
        },
        // Adds events related with removing series
        addCustomEvents: function (baseSeries, volumeSeries) {
            var indicator = this;

            function toEmptyIndicator() {
                indicator.chart.redraw();

                indicator.setData([]);
                indicator.zoneStarts = [];

                if (indicator.zoneLinesSVG) {
                    indicator.zoneLinesSVG.destroy();
                    delete indicator.zoneLinesSVG;
                }
            }

            // If base series is deleted, indicator series data is filled with
            // an empty array
            indicator.dataEventsToUnbind.push(
                addEvent(baseSeries, 'remove', function () {
                    toEmptyIndicator();
                })
            );

            // If volume series is deleted, indicator series data is filled with
            // an empty array
            if (volumeSeries) {
                indicator.dataEventsToUnbind.push(
                    addEvent(volumeSeries, 'remove', function () {
                        toEmptyIndicator();
                    })
                );
            }

            return indicator;
        },
        // Initial animation
        animate: function (init) {
            var series = this,
                attr = {};

            if (H.svg && !init) {
                attr.translateX = series.yAxis.pos;
                series.group.animate(
                    attr,
                    H.extend(H.animObject(series.options.animation), {
                        step: function (val, fx) {
                            series.group.attr({
                                scaleX: Math.max(0.001, fx.pos)
                            });
                        }
                    })
                );

                // Delete this function to allow it only once
                series.animate = null;
            }
        },
        drawPoints: function () {
            var indicator = this;

            if (indicator.options.volumeDivision.enabled) {
                indicator.posNegVolume(true, true);
                columnPrototype.drawPoints.apply(indicator, arguments);
                indicator.posNegVolume(false, false);
            }

            columnPrototype.drawPoints.apply(indicator, arguments);
        },
        // Function responsible for dividing volume into positive and negative
        posNegVolume: function (initVol, pos) {
            var indicator = this,
                signOrder = pos ?
                    ['positive', 'negative'] :
                    ['negative', 'positive'],
                volumeDivision = indicator.options.volumeDivision,
                pointLength = indicator.points.length,
                posWidths = [],
                negWidths = [],
                i = 0,
                pointWidth,
                priceZone,
                wholeVol,
                point;

            if (initVol) {
                indicator.posWidths = posWidths;
                indicator.negWidths = negWidths;
            } else {
                posWidths = indicator.posWidths;
                negWidths = indicator.negWidths;
            }

            for (; i < pointLength; i++) {
                point = indicator.points[i];
                point[signOrder[0] + 'Graphic'] = point.graphic;
                point.graphic = point[signOrder[1] + 'Graphic'];

                if (initVol) {
                    pointWidth = point.shapeArgs.width;
                    priceZone = indicator.priceZones[i];
                    wholeVol = priceZone.wholeVolumeData;

                    if (wholeVol) {
                        posWidths.push(
                            pointWidth / wholeVol * priceZone.positiveVolumeData
                        );
                        negWidths.push(
                            pointWidth / wholeVol * priceZone.negativeVolumeData
                        );
                    } else {
                        posWidths.push(0);
                        negWidths.push(0);
                    }
                }

                point.color = pos ?
                    volumeDivision.styles.positiveColor :
                    volumeDivision.styles.negativeColor;
                point.shapeArgs.width = pos ?
                    indicator.posWidths[i] :
                    indicator.negWidths[i];
                point.shapeArgs.x = pos ?
                    point.shapeArgs.x :
                    indicator.posWidths[i];
            }
        },
        translate: function () {
            var indicator = this,
                options = indicator.options,
                chart = indicator.chart,
                yAxis = indicator.yAxis,
                yAxisMin = yAxis.min,
                zoneLinesOptions = indicator.options.zoneLines,
                priceZones = indicator.priceZones,
                yBarOffset = 0,
                indicatorPoints,
                volumeDataArray,
                maxVolume,
                primalBarWidth,
                barHeight,
                barHeightP,
                oldBarHeight,
                barWidth,
                pointPadding,
                chartPlotTop,
                barX,
                barY;

            columnPrototype.translate.apply(indicator);
            indicatorPoints = indicator.points;

            // Do translate operation when points exist
            if (indicatorPoints.length) {
                pointPadding = options.pointPadding < 0.5 ?
                    options.pointPadding :
                    0.1;
                volumeDataArray = indicator.volumeDataArray;
                maxVolume = H.arrayMax(volumeDataArray);
                primalBarWidth = chart.plotWidth / 2;
                chartPlotTop = chart.plotTop;
                barHeight = abs(yAxis.toPixels(yAxisMin) -
                    yAxis.toPixels(yAxisMin + indicator.rangeStep));
                oldBarHeight = abs(yAxis.toPixels(yAxisMin) -
                    yAxis.toPixels(yAxisMin + indicator.rangeStep));

                if (pointPadding) {
                    barHeightP = abs(barHeight * (1 - 2 * pointPadding));
                    yBarOffset = abs((barHeight - barHeightP) / 2);
                    barHeight = abs(barHeightP);
                }

                indicatorPoints.forEach(function (point, index) {
                    barX = point.barX = point.plotX = 0;
                    barY = point.plotY = (
                        yAxis.toPixels(priceZones[index].start) -
                        chartPlotTop -
                        (
                            yAxis.reversed ?
                                (barHeight - oldBarHeight) :
                                barHeight
                        ) -
                        yBarOffset
                    );
                    barWidth = correctFloat(
                        primalBarWidth *
                        priceZones[index].wholeVolumeData / maxVolume
                    );
                    point.pointWidth = barWidth;

                    point.shapeArgs = indicator.crispCol.apply( // eslint-disable-line no-useless-call
                        indicator,
                        [barX, barY, barWidth, barHeight]
                    );

                    point.volumeNeg = priceZones[index].negativeVolumeData;
                    point.volumePos = priceZones[index].positiveVolumeData;
                    point.volumeAll = priceZones[index].wholeVolumeData;
                });

                if (zoneLinesOptions.enabled) {
                    indicator.drawZones(
                        chart,
                        yAxis,
                        indicator.zoneStarts,
                        zoneLinesOptions.styles
                    );
                }
            }
        },
        getValues: function (series, params) {
            var indicator = this,
                xValues = series.processedXData,
                yValues = series.processedYData,
                chart = indicator.chart,
                ranges = params.ranges,
                VBP = [],
                xData = [],
                yData = [],
                isOHLC,
                volumeSeries,
                priceZones;

            // Checks if base series exists
            if (!series.chart) {
                return H.error(
                    'Base series not found! In case it has been removed, add ' +
                    'a new one.',
                    true,
                    chart
                );
            }

            // Checks if volume series exists
            if (!(volumeSeries = chart.get(params.volumeSeriesID))) {
                return H.error(
                    'Series ' +
                    params.volumeSeriesID +
                    ' not found! Check `volumeSeriesID`.',
                    true,
                    chart
                );
            }

            // Checks if series data fits the OHLC format
            isOHLC = isArray(yValues[0]);

            if (isOHLC && yValues[0].length !== 4) {
                return H.error(
                    'Type of ' +
                    series.name +
                    ' series is different than line, OHLC or candlestick.',
                    true,
                    chart
                );
            }

            // Price zones contains all the information about the zones (index,
            // start, end, volumes, etc.)
            priceZones = indicator.priceZones = indicator.specifyZones(
                isOHLC,
                xValues,
                yValues,
                ranges,
                volumeSeries
            );

            priceZones.forEach(function (zone, index) {
                VBP.push([zone.x, zone.end]);
                xData.push(VBP[index][0]);
                yData.push(VBP[index][1]);
            });

            return {
                values: VBP,
                xData: xData,
                yData: yData
            };
        },
        // Specifing where each zone should start ans end
        specifyZones: function (
            isOHLC,
            xValues,
            yValues,
            ranges,
            volumeSeries
        ) {
            var indicator = this,
                rangeExtremes = isOHLC ? arrayExtremesOHLC(yValues) : false,
                lowRange = rangeExtremes ?
                    rangeExtremes.min :
                    H.arrayMin(yValues),
                highRange = rangeExtremes ?
                    rangeExtremes.max :
                    H.arrayMax(yValues),
                zoneStarts = indicator.zoneStarts = [],
                priceZones = [],
                i = 0,
                j = 1,
                rangeStep,
                zoneStartsLength;

            if (!lowRange || !highRange) {
                if (this.points.length) {
                    this.setData([]);
                    this.zoneStarts = [];
                    this.zoneLinesSVG.destroy();
                }
                return [];
            }

            rangeStep = indicator.rangeStep =
                correctFloat(highRange - lowRange) / ranges;
            zoneStarts.push(lowRange);

            for (; i < ranges - 1; i++) {
                zoneStarts.push(correctFloat(zoneStarts[i] + rangeStep));
            }

            zoneStarts.push(highRange);
            zoneStartsLength = zoneStarts.length;

            //    Creating zones
            for (; j < zoneStartsLength; j++) {
                priceZones.push({
                    index: j - 1,
                    x: xValues[0],
                    start: zoneStarts[j - 1],
                    end: zoneStarts[j]
                });
            }

            return indicator.volumePerZone(
                isOHLC,
                priceZones,
                volumeSeries,
                xValues,
                yValues
            );
        },
        // Calculating sum of volume values for a specific zone
        volumePerZone: function (
            isOHLC,
            priceZones,
            volumeSeries,
            xValues,
            yValues
        ) {
            var indicator = this,
                volumeXData = volumeSeries.processedXData,
                volumeYData = volumeSeries.processedYData,
                lastZoneIndex = priceZones.length - 1,
                baseSeriesLength = yValues.length,
                volumeSeriesLength = volumeYData.length,
                previousValue,
                startFlag,
                endFlag,
                value,
                i;

            // Checks if each point has a corresponding volume value
            if (abs(baseSeriesLength - volumeSeriesLength)) {
                // If the first point don't have volume, add 0 value at the
                // beggining of the volume array
                if (xValues[0] !== volumeXData[0]) {
                    volumeYData.unshift(0);
                }

                // If the last point don't have volume, add 0 value at the end
                // of the volume array
                if (
                    xValues[baseSeriesLength - 1] !==
                    volumeXData[volumeSeriesLength - 1]
                ) {
                    volumeYData.push(0);
                }
            }

            indicator.volumeDataArray = [];

            priceZones.forEach(function (zone) {
                zone.wholeVolumeData = 0;
                zone.positiveVolumeData = 0;
                zone.negativeVolumeData = 0;

                for (i = 0; i < baseSeriesLength; i++) {
                    startFlag = false;
                    endFlag = false;
                    value = isOHLC ? yValues[i][3] : yValues[i];
                    previousValue = i ?
                        (isOHLC ? yValues[i - 1][3] : yValues[i - 1]) :
                        value;

                    // Checks if this is the point with the lowest close value
                    // and if so, adds it calculations
                    if (value <= zone.start && zone.index === 0) {
                        startFlag = true;
                    }

                    // Checks if this is the point with the highest close value
                    // and if so, adds it calculations
                    if (value >= zone.end && zone.index === lastZoneIndex) {
                        endFlag = true;
                    }

                    if (
                        (value > zone.start || startFlag) &&
                        (value < zone.end || endFlag)
                    ) {
                        zone.wholeVolumeData += volumeYData[i];

                        if (previousValue > value) {
                            zone.negativeVolumeData += volumeYData[i];
                        } else {
                            zone.positiveVolumeData += volumeYData[i];
                        }
                    }
                }
                indicator.volumeDataArray.push(zone.wholeVolumeData);
            });

            return priceZones;
        },
        // Function responsoble for drawing additional lines indicating zones
        drawZones: function (chart, yAxis, zonesValues, zonesStyles) {
            var indicator = this,
                renderer = chart.renderer,
                zoneLinesSVG = indicator.zoneLinesSVG,
                zoneLinesPath = [],
                leftLinePos = 0,
                rightLinePos = chart.plotWidth,
                verticalOffset = chart.plotTop,
                verticalLinePos;

            zonesValues.forEach(function (value) {
                verticalLinePos = yAxis.toPixels(value) - verticalOffset;
                zoneLinesPath = zoneLinesPath.concat(chart.renderer.crispLine([
                    'M',
                    leftLinePos,
                    verticalLinePos,
                    'L',
                    rightLinePos,
                    verticalLinePos
                ], zonesStyles.lineWidth));
            });

            // Create zone lines one path or update it while animating
            if (zoneLinesSVG) {
                zoneLinesSVG.animate({
                    d: zoneLinesPath
                });
            } else {
                zoneLinesSVG = indicator.zoneLinesSVG =
                    renderer.path(zoneLinesPath).attr({
                        'stroke-width': zonesStyles.lineWidth,
                        'stroke': zonesStyles.color,
                        'dashstyle': zonesStyles.dashStyle,
                        'zIndex': indicator.group.zIndex + 0.1
                    })
                        .add(indicator.group);
            }
        }
    },
    /**
     * @lends Highcharts.Point#
     */
    {
        // Required for destroying negative part of volume
        destroy: function () {
            if (this.negativeGraphic) {
                this.negativeGraphic = this.negativeGraphic.destroy();
            }
            return H.Point.prototype.destroy.apply(this, arguments);
        }
    }
);

/**
 * A `Volume By Price (VBP)` series. If the [type](#series.vbp.type) option is
 * not specified, it is inherited from [chart.type](#chart.type).
 *
 * @extends   series,plotOptions.vbp
 * @since     6.0.0
 * @product   highstock
 * @excluding dataParser, dataURL
 * @apioption series.vbp
 */
